package org.visionlibrary.image.filters.clustering;

import java.util.ArrayList;
import java.util.List;

import javax.media.jai.TiledImage;

import org.visionlibrary.image.acquisition.ImageFactory;
import org.visionlibrary.image.colormodel.HSVConverter;
import org.visionlibrary.image.colormodel.RGBConverter;
import org.visionlibrary.image.filters.clustering.model.Centroid;
import org.visionlibrary.image.filters.clustering.model.Cluster;
import org.visionlibrary.image.filters.clustering.model.Sample;
import org.visionlibrary.image.model.AbstractFilter;


public class KMeansHSVClusterization extends AbstractFilter {
	protected double hQ = 12;
	protected double sQ = 0.125;
	protected double vQ = 0.125;

	protected double kMax = 10; // maximal number of clusters
	protected double tHMax = 25; // minimal maximal distance
	// between bins and centroids

	protected int iterations = 10; // number of iteration to make

	protected List<Cluster> clusters = null;

	public KMeansHSVClusterization() {}
	
	public KMeansHSVClusterization(double hueQuantizeValue,
			double satQuantizeValue, double valQuantizeValue, double maximumClusters,
			double minDistBetweenCentroids, int iterations) {
		super();
		this.hQ = hueQuantizeValue;
		this.sQ = satQuantizeValue;
		this.vQ = valQuantizeValue;
		this.kMax = maximumClusters;
		this.tHMax = minDistBetweenCentroids;
		this.iterations = iterations;
	}

	@Override
	public TiledImage applyFilter(TiledImage src, TiledImage dest) {
		if (null == src)
			throw new NullPointerException("Source image is null.");

		if (null == dest)
			dest = (new ImageFactory()).createCompatibleImage(src);

		int hueQuantLevels = (int) Math.ceil(360 / hQ) + 1;
		int satQuantLevels = (int) Math.ceil(1.0d / sQ) + 1;
		int valQuantLevels = (int) Math.ceil(1.0d / vQ) + 1;

		double[][][] colorHisto = new double[hueQuantLevels][satQuantLevels][valQuantLevels];

		int maxX = src.getWidth();
		int maxY = src.getHeight();

		for (int x = 0; x < maxX; x++)
			for (int y = 0; y < maxY; y++) {
				float[] values = new float[src.getNumBands()];
				for (int ch = 0; ch < src.getNumBands(); ch++) {
					int pix = src.getSample(x, y, ch);
					values[ch] = (float) norm(0, 255, 0, 1, pix);
				}
				float[] hsv = (new HSVConverter()).convertFromTYPE_RGB(values);
				int qHi = quantize(hsv[0], hQ);
				int qSi = quantize(hsv[1], sQ);
				int qVi = quantize(hsv[2], vQ);
				colorHisto[qHi][qSi][qVi] = colorHisto[qHi][qSi][qVi] + 1;
			}

		double maxColorValue = Double.MIN_VALUE;
		Centroid fstColCentroid = null;

		for (int h = 0; h < colorHisto.length; h++)
			for (int s = 0; s < colorHisto[h].length; s++)
				for (int v = 0; v < colorHisto[h][s].length; v++) {
					double value = colorHisto[h][s][v];
					if (value > maxColorValue) {
						maxColorValue = value;
						fstColCentroid = new Centroid(h, s, v);
					}
				}

		int colCentroidCount = 1;
		clusters = new ArrayList<Cluster>();
		clusters.add(new Cluster(fstColCentroid, hQ));

		while (colCentroidCount <= kMax) {
			double maxGlobalDist = Double.MIN_VALUE;
			Centroid canCentroid = null; // candidate centroid

			for (int h = 0; h < colorHisto.length; h++) {
				for (int s = 0; s < colorHisto[h].length; s++) {
					for (int v = 0; v < colorHisto[h][s].length; v++) {
						if (!isCentroid(h, s, v, clusters)
								&& !isEmpty(h, s, v, colorHisto)) {
							double minLocalDist = Double.MAX_VALUE;
							for (Cluster cluster : clusters) {
								Centroid c = cluster.getCentroid();
								double dist = distance(h, s, v, c);
								if (minLocalDist > dist)
									minLocalDist = dist;
							}
							if (maxGlobalDist < minLocalDist) {
								maxGlobalDist = minLocalDist;
								canCentroid = new Centroid(h, s, v);
							}
						}
					}
				}
			}

			boolean isDistanceCorrect = maxGlobalDist >= tHMax;
			if (!isDistanceCorrect)
				break;

			clusters.add(new Cluster(canCentroid, hQ));
			colCentroidCount++;
		}

		int i = 1;
		while (i <= iterations) {
			for (int h = 0; h < colorHisto.length; h++) {
				for (int s = 0; s < colorHisto[h].length; s++) {
					for (int v = 0; v < colorHisto[h][s].length; v++) {
						if (!isCentroid(h, s, v, clusters)
								&& !isEmpty(h, s, v, colorHisto)) {
							Cluster bestMatch = null;
							double minDist = Double.MAX_VALUE;
							for (Cluster cluster : clusters) {
								Centroid c = cluster.getCentroid();
								double distance = distance(h, s, v, c);
								if (minDist > distance) {
									bestMatch = cluster;
									minDist = distance;
								}
							}
							if (null != bestMatch)
								bestMatch.add(new Sample(h, s, v));
						}
					}
				}
			}

			for (Cluster cluster : clusters)
				cluster.recalculate();
			i++;

			if (i <= iterations)
				for (Cluster cluster : clusters)
					cluster.clear();
		}

		for (int x = 0; x < maxX; x++)
			for (int y = 0; y < maxY; y++) {
				float[] values = new float[src.getNumBands()];
				for (int ch = 0; ch < src.getNumBands(); ch++) {
					int pix = src.getSample(x, y, ch);
					values[ch] = (float) norm(0, 255, 0, 1, pix);
				}

				float[] hsv = (new HSVConverter()).convertFromTYPE_RGB(values);

				hsv = change(hsv);

				values = (new RGBConverter()).convertFromTYPE_HSV(hsv);

				for (int ch = 0; ch < src.getNumBands(); ch++) {
					int pix = (int) norm(0, 1, 0, 255, values[ch]);
					dest.setSample(x, y, ch, pix);
				}
			}

		return dest;
	}

	private float[] change(float[] hsv) {
		float[] result = new float[hsv.length];
		int qVi = quantize(hsv[2], vQ);
		int qSi = quantize(hsv[1], sQ);
		int qHi = quantize(hsv[0], hQ);

		for (Cluster cluster : clusters) {
			List<Sample> samples = cluster.getSamples();
			for (Sample sample : samples) {
				if (sample.hueIndex == qHi && sample.satIndex == qSi
						&& sample.valIndex == qVi) {
					Centroid centroid = cluster.getCentroid();
					result[0] = (float) (centroid.hueIndex * hQ);
					result[1] = (float) (centroid.satIndex * sQ);
					result[2] = (float) (centroid.valIndex * vQ);

					return result;
				}
			}
		}

		return hsv;
	}

	private double distance(int hi, int si, int vi, Centroid centroid) {
		int hj = centroid.hueIndex;
		int sj = centroid.satIndex;
		int vj = centroid.valIndex;
		double d = 0;
		if (Math.abs(hi - hj) > (180 / hQ))
			d = Math.pow((360 / hQ) - Math.abs(hi - hj), 2);
		else
			d = Math.pow(hi - hj, 2);

		double distance = d + Math.pow(si - sj, 2) + Math.pow(vi - vj, 2);

		return distance;
	}

	private boolean isEmpty(int h, int s, int v, double[][][] histogram) {
		return histogram[h][s][v] == 0;
	}

	private boolean isCentroid(int h, int s, int v, List<Cluster> clusters) {
		for (Cluster cluster : clusters) {
			Centroid c = cluster.getCentroid();
			if (c.hueIndex == h && c.satIndex == s && c.valIndex == v)
				return true;
		}
		return false;
	}

	private int quantize(double what, double level) {
		return (int) Math.floor((what / level));
	}

	private double norm(double oMin, double oMax, double nMin, double nMax,
			double value) {
		double ratio = (double) (nMax - nMin) / (double) (oMax - oMin);
		double result = value;
		result -= oMin;
		result *= ratio;
		result += nMin;

		return result;
	}
}